﻿using System;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.ServiceModel;
using System.Text;
using System.Threading;
using System.Xml.Linq;
using Zen4SyncClient.ActivityExecutors;
using Zen4SyncOrchestratorServices;
using Zen4SyncPOCO.ClientTestSessionDescriptorHandler;
using Zen4SyncPOCO.POCO.Admin;
using Zen4SyncPOCO.POCO.Enum.Scenario;
using Zen4SyncPOCO.POCO.Scenario;

namespace Zen4SyncClient
{
    class Program
    {
        /// <summary>
        /// Allows the waiting of the scheduled start time of the Client Session to execute.
        /// </summary>
        private static Timer TestScenarioExecutionStartTimer = null;

        /// <summary>
        /// True if the scheduled start time of the Client Session if about to be reached.
        /// </summary>
        private static bool ExecutionIsAboutToStart = false;

        /// <summary>
        /// Forbids the program to end until the Test Scenario has been executed.
        /// </summary>
        private static EventWaitHandle EventWaitHandle = null;

        /// <summary>
        /// WCF communication towards Orchestrator to allow logging and SQL CE database transmissions.
        /// </summary>
        internal static ChannelFactory<IOrchestratorServices> OrchestratorServices { get; private set; }

        /// <summary>
        /// The ClientSession to execute.
        /// </summary>
        internal static ClientSession ClientSessionToExecute = null;

        /// <summary>
        /// Logger for debugging.
        /// </summary>
        internal static DebugLogger.DebugLogger Logger = null;

        /// <summary>
        /// Debugging log state.
        /// </summary>
        internal static bool DebugLogEnabled = false;

        public static void Main(string[] args)
        {
            Console.WriteLine("Zen4SyncClient\n");

            if (args.Length != 1)
            {
                Usage(args);
                ExitBecauseOfError();
            }

            string cfgEnableDebugLog = ConfigurationManager.AppSettings["Zen4SyncClientEnableDebugLog"];
            string cfgLogFilePath = ConfigurationManager.AppSettings["Zen4SyncClientDebugLogFilePath"];

            if (!String.IsNullOrEmpty(cfgEnableDebugLog) && !String.IsNullOrEmpty(cfgLogFilePath))
            {
                DebugLogEnabled = Boolean.Parse(cfgEnableDebugLog);
            }
            else
            {
                DebugLogEnabled = false;
            }

            if (DebugLogEnabled)
            {
                try
                {
                    string clientTestSessionDescriptorName = Path.GetFileNameWithoutExtension(args[0]);
                    string logFileDirectory = Path.GetDirectoryName(cfgLogFilePath);

                    string uniqueLogFileName = Path.GetFileNameWithoutExtension(cfgLogFilePath) + "_" + clientTestSessionDescriptorName + Path.GetExtension(cfgLogFilePath);

                    Logger = new DebugLogger.DebugLogger(Path.Combine(logFileDirectory, uniqueLogFileName));
                    Logger.Open();
                }
                catch (Exception exc)
                {
                    Console.WriteLine("ERROR while creating debug log file : {0}", exc.Message);
                    Console.WriteLine("Exiting in 15 seconds...");
                    Thread.Sleep(15000);
                    ExitBecauseOfError();
                }
            }

            try
            {
                Console.Write("Parsing ClientTestSessionDescriptor...");
                // Create the POCO representation of the ClientTestSessionDescriptor
                try
                {
                    XElement clientTestSessionDescriptor = XElement.Load(args[0]);
                    IClientTestSessionDescriptorHandler ctsdHandler = ClientTestSessionDescriptorHandlerFactory.Instance;
                    ClientSessionToExecute = ctsdHandler.ReadClientTestSessionDescriptor(clientTestSessionDescriptor);
                    Console.WriteLine("OK");
                }
                catch (Exception exc)
                {
                    Console.WriteLine("ERROR");
                    if (DebugLogEnabled)
                    {
                        Logger.LogError(String.Format("Unable to parse ClientTestSessionDescriptor.\r\nFile Name : {0}\r\nNessage : \r\n{1}\r\nStack Trace : {1}\r\nInner Exception : {2}", args[0], exc.Message, exc.StackTrace, (exc.InnerException != null ? exc.InnerException.Message : "null:")));
                        Console.WriteLine("See {0} for details", Logger.HowToConsultLog);
                    }
                    else
                    {
                        // Give time to read the message
                        Console.WriteLine("Turn on debug logging to see details");
                        DisplayDebugLogActivation();
                        Thread.Sleep(5000);
                    }
                    ExitBecauseOfError();
                }

                // Always instatiate the WCF towards Orchestrator (even if the Client Session does not use log)
                // since we use it no only to log but also to send SQL CE database backup.
                ConfigureWcfToOrchestrator();

                // The EventWaitHandle will allow the waiting for the scheduled start date time
                EventWaitHandle = new EventWaitHandle(false, EventResetMode.ManualReset);

                // Execute the test scenario as soon as the scheduled date time is reached
                ExecuteTestScenarioOnScheduledDateTime(null);

                // Wait until the whole test scenario has been executed
                EventWaitHandle.WaitOne();
            }
            catch (Exception exc)
            {
                Console.WriteLine("ERROR");
                if (DebugLogEnabled)
                {
                    Logger.LogError(String.Format("{0}\n{1}", exc.Message, exc.StackTrace));
                }
                else
                {
                    // Give time to read the message
                    Console.WriteLine("Turn on debug logging to see details");
                    DisplayDebugLogActivation();
                    Thread.Sleep(5000);
                }
            }
            finally
            {
                DisposeResources();
            }
        }

        private static void ExecuteTestScenarioOnScheduledDateTime(object State)
        {
            // Scheduled start date time of the client session is UTC, so we need a UTC now to properly compare
            DateTime nowUtc = DateTime.UtcNow;

            DisplayScheduledStartDateTime();

            DisplayCurrentTime();

            // First time in this method means the client is ready
            if (ClientSessionToExecute.clientSession_enableLog && TestScenarioExecutionStartTimer == null)
            {
                OrchestratorServices.Call(services => services.ClientSessionIsReadytoStart(ClientSessionToExecute.clientSession_id, Process.GetCurrentProcess().Id, DateTime.Now));
            }

            if (TestScenarioExecutionStartTimer == null)
            {
                TestScenarioExecutionStartTimer = new Timer(new TimerCallback(ExecuteTestScenarioOnScheduledDateTime), null, 1000, 1000);
                return;
            }

            // Time to execute
            if (nowUtc >= ClientSessionToExecute.clientSession_scheduledStartDateTime)
            {
                TestScenarioExecutionStartTimer.Dispose();
                TestScenarioExecutionStartTimer = null;

                DisplayTestScenarioExecutionState(0);

                // Execute the test scenario...
                StartTestScenarioExecution();

                // ...and signal it's been executed and the application can exit
                EventWaitHandle.Set();
            }
            else
            {
                long secondsLeft = Convert.ToInt64((ClientSessionToExecute.clientSession_scheduledStartDateTime - nowUtc).TotalSeconds);
                if (secondsLeft < 10 && !ExecutionIsAboutToStart)
                {
                    ExecutionIsAboutToStart = true;
                    TestScenarioExecutionStartTimer.Change(0, 100);
                }

                DisplayTestScenarioExecutionState(secondsLeft);
            }
        }

        /// <summary>
        /// Displays the seconds remaning before the scenario execution start.
        /// If the given number of seconds is zero, displays a message saying that the execution has started.
        /// </summary>
        private static void DisplayTestScenarioExecutionState(long secondsLeftBeforeExecutionStart)
        {
            if (secondsLeftBeforeExecutionStart > 0)
            {
                Console.SetCursorPosition(0, 10);
                Console.WriteLine("Test scenario execution starting in {0:### ##0} seconds", secondsLeftBeforeExecutionStart);
            }
            else
            {
                Console.SetCursorPosition(0, 11);
                Console.WriteLine("Test scenario execution has started...");
            }
        }

        /// <summary>
        /// Starts the execution of the test scenario.
        /// No timers here, it's straight to scenario execution.
        /// </summary>
        private static void StartTestScenarioExecution()
        {
            // Log the actual execution start
            if (ClientSessionToExecute.clientSession_enableLog)
            {
                OrchestratorServices.Call(services => services.ClientSessionStarted(ClientSessionToExecute.clientSession_id, DateTime.Now));
            }

            // Sort activities on execution order
            Activity[] activitiesToExecute = ClientSessionToExecute.clientSession_testScenario.testScenario_activities.OrderBy(act => act.activity_executionOrder).ToArray();

            StringBuilder activityInErrors = new StringBuilder();

            // Execute every activity
            foreach (Activity activity in activitiesToExecute)
            {
                try
                {
                    ActivityContext context = ClientSessionToExecute.clientSession_activityContexts.Where(c => c.activityContext_activityId == activity.activity_id).SingleOrDefault();

                    if (context == null)
                    {
                        throw new Exception(String.Format("No activity context for activity {0}", activity.activity_id));
                    }

                    // Wait before activity execution start if needed
                    if (activity.activity_preWaitingTimeInSeconds != 0)
                    {
                        Thread.Sleep(activity.activity_preWaitingTimeInSeconds * 1000);
                    }

                    // Log activity execution start
                    if (ClientSessionToExecute.clientSession_enableLog)
                    {
                        OrchestratorServices.Call(services => services.ActivityStarted(ClientSessionToExecute.clientSession_id, activity.activity_id, DateTime.Now));
                    }

                    // Execute activity
                    switch (activity.activity_activityTypeEnum)
                    {
                        case ActivityTypeEnum.SyncSession:
                            MergeSync.ExecuteSyncSession(activity.activity_syncSession, context.activityContext_syncSessionContext);
                            break;
                        case ActivityTypeEnum.SqlServerExecution:
                            SqlServer.ExecuteSqlServerExecution(activity.activity_sqlServerExecution, context.activityContext_sqlServerExecutionContext, activity.activity_id);
                            break;
                        case ActivityTypeEnum.SqlServerCheckData:
                            SqlServer.ExecuteSqlServerCheckData(activity.activity_sqlServerCheckData, context.activityContext_sqlServerCheckDataContext);
                            break;
                        case ActivityTypeEnum.SqlCeExecution:
                            SqlCe.ExecuteSqlCeExecution(activity.activity_sqlCeExecution, context.activityContext_sqlCeExecutionContext, activity.activity_id);
                            break;
                        case ActivityTypeEnum.SqlCeCheckData:
                            SqlCe.ExecuteSqlCeCheckData(activity.activity_sqlCeCheckData, context.activityContext_sqlCeCheckDataContext);
                            break;
                        case ActivityTypeEnum.SqlCeBackup:
                            SqlCe.ExecuteSqlCeBackup(activity.activity_sqlCeBackup, context.activityContext_sqlCeBackupContext, activity.activity_id);
                            break;
                        case ActivityTypeEnum.MefExecution:
                            new Mef().ExecuteMefExecution(activity.activity_mefExecution, context.activityContext_mefExecutionContext);
                            break;
                        default:
                            throw new Exception(String.Format("Unknown activity type for activity {0}", activity.activity_id));
                    }

                    // Log activity execution end
                    if (ClientSessionToExecute.clientSession_enableLog)
                    {
                        OrchestratorServices.Call(services => services.ActivityEndedSuccessfully(ClientSessionToExecute.clientSession_id, activity.activity_id, DateTime.Now));
                    }

                    // Wait after activity execution if needed
                    if (activity.activity_postWaitingTimeInSeconds != 0)
                    {
                        Thread.Sleep(activity.activity_postWaitingTimeInSeconds * 1000);
                    }
                }
                catch (Exception exc)
                {
                    // Add the activity to the list so that we know this client session will end with errors.
                    activityInErrors.AppendFormat("Activity Id : {0} of type {1} /", activity.activity_id.ToString(), Enum.GetName(typeof(ActivityTypeEnum), activity.activity_activityTypeEnum));
                    if (ClientSessionToExecute.clientSession_enableLog)
                    {
                        OrchestratorServices.Call(services => services.ActivityEndedWithError(ClientSessionToExecute.clientSession_id, activity.activity_id, DateTime.Now, exc.Message));
                    }
                }
            }
            if (ClientSessionToExecute.clientSession_enableLog)
            {
                if (String.IsNullOrEmpty(activityInErrors.ToString()))
                    OrchestratorServices.Call(services => services.ClientSessionEndedSuccessfully(ClientSessionToExecute.clientSession_id, DateTime.Now));
                else
                    OrchestratorServices.Call(services => services.ClientSessionEndedWithError(ClientSessionToExecute.clientSession_id, DateTime.Now, String.Format("Errors on the following activities : {0}", activityInErrors.ToString())));
            }
        }

        /// <summary>
        /// Displays the current time.
        /// </summary>
        private static void DisplayCurrentTime()
        {
            Console.SetCursorPosition(0, 8);
            Console.WriteLine("Current local time is : {0:dd/MM/yyyy HH:mm:ss}", DateTime.Now);
        }

        /// <summary>
        /// Displays the scheduled start time of the ClientSession to execute.
        /// Note: the displayed time is the scheduled start time converted into local time.
        /// </summary>
        private static void DisplayScheduledStartDateTime()
        {
            Console.SetCursorPosition(0, 7);
            Console.WriteLine("Scheduled start time : {0:dd/MM/yyyy HH:mm:ss}", ClientSessionToExecute.clientSession_scheduledStartDateTime.ToLocalTime());
        }

        /// <summary>
        /// Displays the usage of this program.
        /// </summary>
        private static void Usage(string[] args)
        {
            Console.WriteLine("Zen4SyncClient Usage:\r\n- First argument must be <ClientTestSessionDescriptor XML File Path>");
            Console.WriteLine("Currents arguments are :");
            foreach (string arg in args)
            {
                Console.WriteLine("\t{0}\n", arg);
            }
        }

        /// <summary>
        /// Properly (resources are disposed) exits the Client in case of an error.
        /// </summary>
        private static void ExitBecauseOfError()
        {
            DisposeResources();
            Environment.Exit(1);
        }

        /// <summary>
        /// Displays instructions on how to turn on debug logging.
        /// </summary>
        private static void DisplayDebugLogActivation()
        {
            Console.WriteLine("To turn on debug logging, insert the following section into the App.config file :");
            Console.WriteLine("<configuration>\n\t<appSettings>\n\t\t<add key=\"Zen4SyncClientEnableDebugLog\" value=\"true\"/>\n\t\t<add key=\"Zen4SyncClientDebugLogFilePath\" value=\"PathToLogFile\"/>");
        }

        /// <summary>
        /// Configure Wcf communication to Orchestrator
        /// </summary>
        private static void ConfigureWcfToOrchestrator()
        {
            // Always instatiate the WCF towards Orchestrator (even if the Client Session does not use log)
            // since we use it no only to log but also to send SQL CE database backup.
            Console.Write("Configuring WCF communication with Orchestrator...");
            try
            {
                OrchestratorServices = new ChannelFactory<IOrchestratorServices>(ConfigurationManager.AppSettings["activeOrchestratorEndpointName"]);
                Console.WriteLine("OK");
            }
            catch (Exception exc)
            {
                Console.WriteLine("ERROR");
                if (DebugLogEnabled)
                {
                    Logger.LogError(String.Format("Unable to configure WCF communication towards Orchestrator.\n{0}\n{1}", exc.Message, exc.StackTrace));
                    Console.WriteLine("See {0} for details", Logger.HowToConsultLog);
                }
                else
                {
                    // Give time to read the message
                    Console.WriteLine("Turn on debug logging to see details");
                    DisplayDebugLogActivation();
                    Thread.Sleep(5000);
                }
                ExitBecauseOfError();
            }
        }

        /// <summary>
        /// Disposes all the resources that needs to be : DebugLogger and Orchestrator channel.
        /// </summary>
        private static void DisposeResources()
        {
            if (DebugLogEnabled && Logger.IsOpened) Logger.Close();

            // Close the WCF channel factory
            if (OrchestratorServices != null && OrchestratorServices.State == CommunicationState.Opened)
            {
                OrchestratorServices.Close();
            }
        }
    }
}
